package com.googlecode.jawb;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

import com.googlecode.jawb.AnnotationParser.RTFieldBean;
import com.googlecode.jawb.clues.types.DynamicLengthType;
import com.googlecode.jawb.clues.types.StaticLengthType;

/**
 * Loads data from input stream and maps it to the new object. Call method
 * {@link #process()} and then {@link #getProcessedObject()}.
 */
public class InputProcessor extends Processor {
	private InputStream inputStream = null;
	private JAWB        jawb        = null;
	
	public InputProcessor(InputStream inputStream, Class<?> toClass, JAWB jawb, RTFieldBean... injectedValues) {
		super(newInstance(toClass, injectedValues));
		this.inputStream = inputStream;
		this.jawb        = jawb;
	}

	/**
	 * Returns new instance of a given class and passes given list of values to
	 * the constructor. If no appropriate constructor is found, RuntimeException
	 * is thrown.
	 * 
	 * @param toClass
	 *            Class to be instantiated.
	 * @param injectedValues
	 *            Values (with types) to be passed to the constructor.
	 * @return New instance of a given class.
	 */
	protected static Object newInstance(Class<?> toClass, RTFieldBean... injectedValues) {
		try {
			Class<?>[]  paramTypes  = new Class<?>[injectedValues.length];
			Object[]    paramValues = new Object[injectedValues.length];
			
			for (int i = 0; i < injectedValues.length; i++) {
				paramTypes[i]   = injectedValues[i].getFieldType();
				paramValues[i]  = injectedValues[i].getFieldValue();
			}
			
			Constructor<?> constructor = toClass.getDeclaredConstructor(paramTypes);
			
			constructor.setAccessible(true);
			return constructor.newInstance(paramValues);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException("Invalid injection: Constructor meeting given criteria could not be found.", e);
		} catch (SecurityException | IllegalAccessException e) {
			throw new RuntimeException("Invalid injection: Access policy violation.", e);
		} catch (InstantiationException e) {
			throw new RuntimeException("Invalid injection: Could not instantiate an abstract class.", e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: Invalid argument has been passed while trying to instantiate object with injected arguments", e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException("Invalid injection: Constructor has thrown an exception.", e);
		}
	}

	@Override
	protected void onPrimitiveField(Field field, BinaryType asType) {
		try {
			long    longValue = asType.loadNumericValue(inputStream);
			Object  converted = Type.getTypeOf(field.getType()).castFromLong(longValue);
			
			field.set(super.processedObject, converted);
		} catch (IOException e) {
			this.setError(e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: An invalid argument has been passed while trying to set a field.", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Internal error: Field could not be set due to access policy.", e);
		}
	}

	@Override
	protected void onObjectField(Field field, Class<?> asClass) {
		try {
			Object createdObject = jawb.loadFrom(inputStream, asClass, super.annotationParser.getInjectedValues());
			field.set(super.processedObject, createdObject);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: An invalid argument has been passed while trying to set a field.", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Internal error: Field could not be set due to access policy.", e);
		} catch (IOException e) {
			this.setError(e);
			this.kill();
		}
	}

	@Override
	protected void onPrimitiveArray(Field field, int length, BinaryType asType) {
		try {
			if (!field.getType().isArray()) {
				throw new RuntimeException(
					"Field " + field.getName() + " is requested to be loaded as an array, but it has different type."
				);
			}
			
			Object array = Array.newInstance(field.getType().getComponentType(), length);
			
			if (length == 0) {
				length = inputStream.available();
			}
			
			for (int i = 0; i < length; i++) {
				long    longValue   = asType.loadNumericValue(inputStream);
				Object  converted   = Type.getTypeOf(field.getType().getComponentType()).castFromLong(longValue);
				
				Array.set(array, i, converted);
				i = annotationParser.callItemPostSetListener(i, ListenerTarget.INPUT);
			}
			
			field.set(processedObject, array);
		} catch (IOException e) {
			this.setError(e);
			this.kill();
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: An invalid argument has been passed while trying to set a field.", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Internal error: Field could not be set due to access policy.", e);
		}
	}

	@Override
	protected void onObjectArray(Field field, int length, Class<?> asClass) {
		try {
			if (!field.getType().isArray()) {
				throw new RuntimeException(
					"Field " + field.getName() + " is requested to be loaded as an array, but it has different type."
				);
			}
			
			Object array = Array.newInstance(field.getType().getComponentType(), length);

			if (length == 0) {
				length = inputStream.available();
			}
			
			for (int i = 0; i < length; i++) {
				Object createdObject = jawb.loadFrom(inputStream, asClass, super.annotationParser.getInjectedValues());
				Array.set(array, i, createdObject);
				i = annotationParser.callItemPostSetListener(i, ListenerTarget.INPUT);
			}
			
			field.set(processedObject, array);
		} catch (IOException e) {
			this.setError(e);
			this.kill();
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: An invalid argument has been passed while trying to set a field.", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Internal error: Field could not be set due to access policy.", e);
		}
	}
	
	@Override
	protected void onStaticallyConvertedField(Field field, StaticLengthType staticLengthType) {
		try {
			byte[] rawBytes = new byte[staticLengthType.getAcceptedLength()];
			inputStream.read(rawBytes);
			
			Object convertedValue = staticLengthType.fromByteArray(rawBytes);
			field.set(processedObject, convertedValue);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: An invalid argument has been passed while trying to set a field (custom static converter)", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Internal error: Field could not be set due to access policy (custom static converter).", e);
		} catch (IOException e) {
			this.setError(e);
			this.kill();
		}
	}
	
	@Override
	protected void onDynamicallyConvertedField(Field field, DynamicLengthType dynamicLengthType) {
		try {
			Object convertedValue = dynamicLengthType.fromByteArray(inputStream);
			
			field.set(processedObject, convertedValue);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException("Internal error: An invalid argument has been passed while trying to set a field (custom dynamic converter)", e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException("Internal error: Field could not be set due to access policy (custom dynamic converter).", e);
		}		
	}

	/**
	 * Returns processed object. Throws exception which has occurred.
	 * 
	 * @return Object which was processed.
	 * @throws IOException
	 */
	protected Object getResult() throws IOException {
		if (!hasError()) {
			return processedObject;
		} else {
			if (getError() instanceof IOException) {
				throw (IOException) getError();
			} else {
				throw new RuntimeException("Unknown error has occurred.", getError());
			}
		}
	}
}
